﻿
namespace Orchard.OData {
    using Microsoft.OData.Client;
    using Microsoft.OData.Core;
    using Microsoft.OData.Edm;
    using Microsoft.OData.Edm.Library;
    using Microsoft.Spatial;
    using Orchard.OData.Services;
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Data.Linq;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Web;
    using System.Web.OData;
    using System.Xml.Linq;

    internal static class ODataExtensions {
        private static readonly Dictionary<Type, IEdmPrimitiveType> _builtInTypesMapping =
            new Dictionary<Type, IEdmPrimitiveType> {
            { typeof(string), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String) },
            { typeof(bool), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean) },
            { typeof(bool?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Boolean) },
            { typeof(byte), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Byte) },
            { typeof(byte?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Byte) },
            { typeof(DateTime), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset) },
            { typeof(DateTime?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset) },
            { typeof(decimal), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Decimal) },
            { typeof(decimal?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Decimal) },
            { typeof(double), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double) },
            { typeof(double?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Double) },
            { typeof(Guid), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid) },
            { typeof(Guid?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Guid) },
            { typeof(short), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int16) },
            { typeof(short?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int16) },
            { typeof(int), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32) },
            { typeof(int?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32) },
            { typeof(long), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64) },
            { typeof(long?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64) },
            { typeof(sbyte), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.SByte) },
            { typeof(sbyte?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.SByte) },
            { typeof(float), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single) },
            { typeof(float?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Single) },
            { typeof(byte[]), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Binary) },
            { typeof(Stream), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Stream) },
            { typeof(Geography), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Geography) },
            { typeof(GeographyPoint), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyPoint) },
            { typeof(GeographyLineString), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyLineString) },
            { typeof(GeographyPolygon), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyPolygon) },
            { typeof(GeographyCollection), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyCollection) },
            { typeof(GeographyMultiLineString), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyMultiLineString) },
            { typeof(GeographyMultiPoint), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyMultiPoint) },
            { typeof(GeographyMultiPolygon), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyMultiPolygon) },
            { typeof(Geometry), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Geometry) },
            { typeof(GeometryPoint), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeometryPoint) },
            { typeof(GeometryLineString), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeometryLineString) },
            { typeof(GeometryPolygon), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeometryPolygon) },
            { typeof(GeometryCollection), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeometryCollection) },
            { typeof(GeometryMultiLineString), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeometryMultiLineString) },
            { typeof(GeometryMultiPoint), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeometryMultiPoint) },
            { typeof(GeometryMultiPolygon), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeometryMultiPolygon) },
            { typeof(TimeSpan), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Duration) },
            { typeof(TimeSpan?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Duration) },
            { typeof(DateTimeOffset), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset) },
            { typeof(DateTimeOffset?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.DateTimeOffset) },
            { typeof(XElement), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String) },
            { typeof(ushort), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32) },
            { typeof(ushort?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32) },
            { typeof(uint), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64) },
            { typeof(uint?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64) },
            { typeof(ulong), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64) },
            { typeof(ulong?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int64) },
            { typeof(char[]), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String) },
            { typeof(char), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String) },
            { typeof(char?), EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String) }};

        private static IEdmPrimitiveType GetEdmPrimitiveTypeOrNull(Type clrType) {
            Type underlyingType = Nullable.GetUnderlyingType(clrType) ?? clrType;
            if (underlyingType.IsEnum){
                underlyingType = typeof(string);
            }
            IEdmPrimitiveType primitiveType;
            return _builtInTypesMapping.TryGetValue(underlyingType, out primitiveType) ? primitiveType : null;
        }

        private static IEdmType GetEdmType(this IEdmModel edmModel, Type clrType) {
            IEdmPrimitiveType primitiveType = GetEdmPrimitiveTypeOrNull(clrType);
            if (primitiveType != null) {
                return primitiveType;
            }

            Type enumerableOfT = TypeSystem.GetIEnumerableElementType(clrType);
            if (enumerableOfT != null) {
                IEdmTypeReference elementType = GetEdmTypeReference(edmModel, enumerableOfT);
                if (elementType != null) {
                    return new EdmCollectionType(elementType);
                }
            }

            return edmModel.FindType(MetadataBuilder._namespace + "." + clrType.Name);
        }

        internal static IEdmTypeReference GetEdmTypeReference(this IEdmModel edmModel, Type clrType) {
            IEdmType edmType = edmModel.GetEdmType(clrType);
            if (edmType == null) {
                return null;
            }

            bool isNullable = !clrType.IsValueType || Nullable.GetUnderlyingType(clrType) != null;
            switch (edmType.TypeKind) {
                case EdmTypeKind.Collection:
                    return new EdmCollectionTypeReference(edmType as IEdmCollectionType);
                case EdmTypeKind.Complex:
                    return new EdmComplexTypeReference(edmType as IEdmComplexType, isNullable);
                case EdmTypeKind.Entity:
                    return new EdmEntityTypeReference(edmType as IEdmEntityType, isNullable);
                case EdmTypeKind.EntityReference:
                    return new EdmEntityReferenceTypeReference(edmType as IEdmEntityReferenceType, isNullable);
                case EdmTypeKind.Enum:
                    return new EdmEnumTypeReference(edmType as IEdmEnumType, isNullable);
                case EdmTypeKind.Primitive:
                    return EdmCoreModel.Instance.GetPrimitive((edmType as IEdmPrimitiveType).PrimitiveKind, isNullable);
                default:
                    throw new NotSupportedException(string.Format("{0} is not an supported edm type.", edmType.ToTraceString()));
            }
        }

        internal static Dictionary<string, string> ToDictionary(this NameValueCollection nameValueCollection){
            return nameValueCollection
                .AllKeys
                .Select(k => new { Key = k, Value = nameValueCollection[k] })
                .Where(kvp => !string.IsNullOrEmpty(kvp.Value))
                .ToDictionary(p => p.Key, p => p.Value);
        }

        internal static Uri ODataRootService(this HttpRequest httpRequest) {
            var requestFragments = httpRequest.Url.AbsoluteUri.Split("/".ToArray());
            var rootFragments = requestFragments.TakeWhile(fragment => !fragment.ToLower().StartsWith("odata"));
            var rootUrl = string.Join("/", rootFragments);
            return new Uri(rootUrl + "/odata");
        }

        internal static ODataMessageWriter GetODataMessageWriter(this HttpContext httpContext, HttpStatusCode statusCode, string contentTypeHeader, IEdmModel edmModel) {
            var httpRequest = httpContext.Request;
            var httpResponse = httpContext.Response;

            httpResponse.Headers.Add(ODataConstants.ContentTypeHeader, contentTypeHeader);
            
            var httpWebResponseMessage = new HttpWebResponseMessage(
                httpResponse.Headers.ToDictionary(),
                (int)statusCode,
                () => httpResponse.OutputStream) as IODataResponseMessage;
            
            var settings = new ODataMessageWriterSettings();
            //settings.SetContentType(ODataFormat.Json);
            settings.ODataUri = new ODataUri() { ServiceRoot = httpRequest.ODataRootService() };
            
            return new ODataMessageWriter((IODataResponseMessage)httpWebResponseMessage, settings, edmModel);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////
        // Copyright (c) Microsoft Corporation.  All rights reserved.
        // Licensed under the MIT License.  See License.txt in the project root for license information.
        // System.Web.OData/OData/Formatter/Serialization/ODataPrimitiveSerializer.cs
        internal static object ConvertUnsupportedPrimitives(this object value)
        {
            if (value == null) {
                return null;
            }

            Type type = value.GetType();
            // Note that type cannot be a nullable type as value is not null and it is boxed.
            switch (Type.GetTypeCode(type)) {
                case TypeCode.Char:
                    return new String((char)value, 1);

                case TypeCode.UInt16:
                    return (int)(ushort)value;

                case TypeCode.UInt32:
                    return (long)(uint)value;

                case TypeCode.UInt64:
                    return checked((long)(ulong)value);

                case TypeCode.DateTime:
                    DateTime dateTime = (DateTime)value;
                    TimeZoneInfo timeZone = TimeZoneInfoHelper.TimeZone;
                    
                    if (dateTime.Kind == DateTimeKind.Utc || dateTime.Kind == DateTimeKind.Local) {
                        return new DateTimeOffset(dateTime.ToUniversalTime()).ToOffset(timeZone.BaseUtcOffset);
                    }

                    DateTimeOffset dateTimeOffset = new DateTimeOffset(dateTime, timeZone.GetUtcOffset(dateTime));
                    return dateTimeOffset.ToUniversalTime().ToOffset(timeZone.BaseUtcOffset);

                default:
                    if (type.IsEnum) {
                        return value.ToString();
                    }
                    if (type == typeof(char[])) {
                        return new String(value as char[]);
                    }
                    if (type == typeof(XElement)) {
                        return ((XElement)value).ToString();
                    }
                    if (type == typeof(Binary)) {
                        return ((Binary)value).ToArray();
                    }
                    break;
            }

            return value;
        }
    }
}